<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Server
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */

/**
 * Zend_Server_Reflection_Node
 */
require_once 'Zend/Server/Reflection/Node.php';

/**
 * Zend_Server_Reflection_Parameter
 */
require_once 'Zend/Server/Reflection/Parameter.php';

/**
 * Zend_Server_Reflection_Prototype
 */
require_once 'Zend/Server/Reflection/Prototype.php';

/**
 * Function/Method Reflection
 *
 * Decorates a ReflectionFunction. Allows setting and retrieving an alternate
 * 'service' name (i.e., the name to be used when calling via a service),
 * setting and retrieving the description (originally set using the docblock
 * contents), retrieving the callback and callback type, retrieving additional
 * method invocation arguments, and retrieving the
 * method {@link Zend_Server_Reflection_Prototype prototypes}.
 *
 * @category   Zend
 * @package    Zend_Server
 * @subpackage Reflection
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version $Id: Abstract.php 16210 2009-06-21 19:22:17Z thomas $
 */
abstract class Zend_Server_Reflection_Function_Abstract
{
	/**
	 * @var ReflectionFunction
	 */
	protected $_reflection;

	/**
	 * Additional arguments to pass to method on invocation
	 * @var array
	 */
	protected $_argv = array();

	/**
	 * Used to store extra configuration for the method (typically done by the
	 * server class, e.g., to indicate whether or not to instantiate a class).
	 * Associative array; access is as properties via {@link __get()} and
	 * {@link __set()}
	 * @var array
	 */
	protected $_config = array();

	/**
	 * Declaring class (needed for when serialization occurs)
	 * @var string
	 */
	protected $_class;

	/**
	 * Function/method description
	 * @var string
	 */
	protected $_description = '';

	/**
	 * Namespace with which to prefix function/method name
	 * @var string
	 */
	protected $_namespace;

	/**
	 * Prototypes
	 * @var array
	 */
	protected $_prototypes = array();

	private $_return;
	private $_returnDesc;
	private $_paramDesc;
	private $_sigParams;
	private $_sigParamsDepth;

	/**
	 * Constructor
	 *
	 * @param ReflectionFunction $r
	 */
	public function __construct(Reflector $r, $namespace = null, $argv = array())
	{
		// In PHP 5.1.x, ReflectionMethod extends ReflectionFunction. In 5.2.x,
		// both extend ReflectionFunctionAbstract. So, we can't do normal type
		// hinting in the prototype, but instead need to do some explicit
		// testing here.
		if ((!$r instanceof ReflectionFunction)
		&& (!$r instanceof ReflectionMethod)) {
			require_once 'Zend/Server/Reflection/Exception.php';
			throw new Zend_Server_Reflection_Exception('Invalid reflection class');
		}
		$this->_reflection = $r;

		// Determine namespace
		if (null !== $namespace){
			$this->setNamespace($namespace);
		}

		// Determine arguments
		if (is_array($argv)) {
			$this->_argv = $argv;
		}

		// If method call, need to store some info on the class
		if ($r instanceof ReflectionMethod) {
			$this->_class = $r->getDeclaringClass()->getName();
		}

		// Perform some introspection
		$this->_reflect();
	}

	/**
	 * Create signature node tree
	 *
	 * Recursive method to build the signature node tree. Increments through
	 * each array in {@link $_sigParams}, adding every value of the next level
	 * to the current value (unless the current value is null).
	 *
	 * @param Zend_Server_Reflection_Node $parent
	 * @param int $level
	 * @return void
	 */
	protected function _addTree(Zend_Server_Reflection_Node $parent, $level = 0)
	{
		if ($level >= $this->_sigParamsDepth) {
			return;
		}

		foreach ($this->_sigParams[$level] as $value) {
			$node = new Zend_Server_Reflection_Node($value, $parent);
			if ((null !== $value) && ($this->_sigParamsDepth > $level + 1)) {
				$this->_addTree($node, $level + 1);
			}
		}
	}

	/**
	 * Build the signature tree
	 *
	 * Builds a signature tree starting at the return values and descending
	 * through each method argument. Returns an array of
	 * {@link Zend_Server_Reflection_Node}s.
	 *
	 * @return array
	 */
	protected function _buildTree()
	{
		$returnTree = array();
		foreach ((array) $this->_return as $value) {
			$node = new Zend_Server_Reflection_Node($value);
			$this->_addTree($node);
			$returnTree[] = $node;
		}

		return $returnTree;
	}

	/**
	 * Build method signatures
	 *
	 * Builds method signatures using the array of return types and the array of
	 * parameters types
	 *
	 * @param array $return Array of return types
	 * @param string $returnDesc Return value description
	 * @param array $params Array of arguments (each an array of types)
	 * @param array $paramDesc Array of parameter descriptions
	 * @return array
	 */
	protected function _buildSignatures($return, $returnDesc, $paramTypes, $paramDesc)
	{
		$this->_return         = $return;
		$this->_returnDesc     = $returnDesc;
		$this->_paramDesc      = $paramDesc;
		$this->_sigParams      = $paramTypes;
		$this->_sigParamsDepth = count($paramTypes);
		$signatureTrees        = $this->_buildTree();
		$signatures            = array();

		$endPoints = array();
		foreach ($signatureTrees as $root) {
			$tmp = $root->getEndPoints();
			if (empty($tmp)) {
				$endPoints = array_merge($endPoints, array($root));
			} else {
				$endPoints = array_merge($endPoints, $tmp);
			}
		}

		foreach ($endPoints as $node) {
			if (!$node instanceof Zend_Server_Reflection_Node) {
				continue;
			}

			$signature = array();
			do {
				array_unshift($signature, $node->getValue());
				$node = $node->getParent();
			} while ($node instanceof Zend_Server_Reflection_Node);

			$signatures[] = $signature;
		}

		// Build prototypes
		$params = $this->_reflection->getParameters();
		foreach ($signatures as $signature) {
			$return = new Zend_Server_Reflection_ReturnValue(array_shift($signature), $this->_returnDesc);
			$tmp    = array();
			foreach ($signature as $key => $type) {
				$param = new Zend_Server_Reflection_Parameter($params[$key], $type, $this->_paramDesc[$key]);
				$param->setPosition($key);
				$tmp[] = $param;
			}

			$this->_prototypes[] = new Zend_Server_Reflection_Prototype($return, $tmp);
		}
	}

	/**
	 * Use code reflection to create method signatures
	 *
	 * Determines the method help/description text from the function DocBlock
	 * comment. Determines method signatures using a combination of
	 * ReflectionFunction and parsing of DocBlock @param and @return values.
	 *
	 * @param ReflectionFunction $function
	 * @return array
	 */
	protected function _reflect()
	{
		$function           = $this->_reflection;
		$helpText           = '';
		$signatures         = array();
		$returnDesc         = '';
		$paramCount         = $function->getNumberOfParameters();
		$paramCountRequired = $function->getNumberOfRequiredParameters();
		$parameters         = $function->getParameters();
		$docBlock           = $function->getDocComment();

		if (!empty($docBlock)) {
			// Get help text
			if (preg_match(':/\*\*\s*\r?\n\s*\*\s(.*?)\r?\n\s*\*(\s@|/):s', $docBlock, $matches))
			{
				$helpText = $matches[1];
				$helpText = preg_replace('/(^\s*\*\s)/m', '', $helpText);
				$helpText = preg_replace('/\r?\n\s*\*\s*(\r?\n)*/s', "\n", $helpText);
				$helpText = trim($helpText);
			}

			// Get return type(s) and description
			$return     = 'void';
			if (preg_match('/@return\s+(\S+)/', $docBlock, $matches)) {
				$return = explode('|', $matches[1]);
				if (preg_match('/@return\s+\S+\s+(.*?)(@|\*\/)/s', $docBlock, $matches))
				{
					$value = $matches[1];
					$value = preg_replace('/\s?\*\s/m', '', $value);
					$value = preg_replace('/\s{2,}/', ' ', $value);
					$returnDesc = trim($value);
				}
			}

			// Get param types and description
			if (preg_match_all('/@param\s+([^\s]+)/m', $docBlock, $matches)) {
				$paramTypesTmp = $matches[1];
				if (preg_match_all('/@param\s+\S+\s+(\$^\S+)\s+(.*?)(@|\*\/)/s', $docBlock, $matches))
				{
					$paramDesc = $matches[2];
					foreach ($paramDesc as $key => $value) {
						$value = preg_replace('/\s?\*\s/m', '', $value);
						$value = preg_replace('/\s{2,}/', ' ', $value);
						$paramDesc[$key] = trim($value);
					}
				}
			}
		} else {
			$helpText = $function->getName();
			$return   = 'void';
		}

		// Set method description
		$this->setDescription($helpText);

		// Get all param types as arrays
		if (!isset($paramTypesTmp) && (0 < $paramCount)) {
			$paramTypesTmp = array_fill(0, $paramCount, 'mixed');
		} elseif (!isset($paramTypesTmp)) {
			$paramTypesTmp = array();
		} elseif (count($paramTypesTmp) < $paramCount) {
			$start = $paramCount - count($paramTypesTmp);
			for ($i = $start; $i < $paramCount; ++$i) {
				$paramTypesTmp[$i] = 'mixed';
			}
		}

		// Get all param descriptions as arrays
		if (!isset($paramDesc) && (0 < $paramCount)) {
			$paramDesc = array_fill(0, $paramCount, '');
		} elseif (!isset($paramDesc)) {
			$paramDesc = array();
		} elseif (count($paramDesc) < $paramCount) {
			$start = $paramCount - count($paramDesc);
			for ($i = $start; $i < $paramCount; ++$i) {
				$paramDesc[$i] = '';
			}
		}

		if (count($paramTypesTmp) != $paramCount) {
			require_once 'Zend/Server/Reflection/Exception.php';
			throw new Zend_Server_Reflection_Exception(
               'Variable number of arguments is not supported for services (except optional parameters). '
               . 'Number of function arguments must correspond to actual number of arguments described in a docblock.');
		}

		$paramTypes = array();
		foreach ($paramTypesTmp as $i => $param) {
			$tmp = explode('|', $param);
			if ($parameters[$i]->isOptional()) {
				array_unshift($tmp, null);
			}
			$paramTypes[] = $tmp;
		}

		$this->_buildSignatures($return, $returnDesc, $paramTypes, $paramDesc);
	}


	/**
	 * Proxy reflection calls
	 *
	 * @param string $method
	 * @param array $args
	 * @return mixed
	 */
	public function __call($method, $args)
	{
		if (method_exists($this->_reflection, $method)) {
			return call_user_func_array(array($this->_reflection, $method), $args);
		}

		require_once 'Zend/Server/Reflection/Exception.php';
		throw new Zend_Server_Reflection_Exception('Invalid reflection method ("' .$method. '")');
	}

	/**
	 * Retrieve configuration parameters
	 *
	 * Values are retrieved by key from {@link $_config}. Returns null if no
	 * value found.
	 *
	 * @param string $key
	 * @return mixed
	 */
	public function __get($key)
	{
		if (isset($this->_config[$key])) {
			return $this->_config[$key];
		}

		return null;
	}

	/**
	 * Set configuration parameters
	 *
	 * Values are stored by $key in {@link $_config}.
	 *
	 * @param string $key
	 * @param mixed $value
	 * @return void
	 */
	public function __set($key, $value)
	{
		$this->_config[$key] = $value;
	}

	/**
	 * Set method's namespace
	 *
	 * @param string $namespace
	 * @return void
	 */
	public function setNamespace($namespace)
	{
		if (empty($namespace)) {
			$this->_namespace = '';
			return;
		}

		if (!is_string($namespace) || !preg_match('/[a-z0-9_\.]+/i', $namespace)) {
			require_once 'Zend/Server/Reflection/Exception.php';
			throw new Zend_Server_Reflection_Exception('Invalid namespace');
		}

		$this->_namespace = $namespace;
	}

	/**
	 * Return method's namespace
	 *
	 * @return string
	 */
	public function getNamespace()
	{
		return $this->_namespace;
	}

	/**
	 * Set the description
	 *
	 * @param string $string
	 * @return void
	 */
	public function setDescription($string)
	{
		if (!is_string($string)) {
			require_once 'Zend/Server/Reflection/Exception.php';
			throw new Zend_Server_Reflection_Exception('Invalid description');
		}

		$this->_description = $string;
	}

	/**
	 * Retrieve the description
	 *
	 * @return void
	 */
	public function getDescription()
	{
		return $this->_description;
	}

	/**
	 * Retrieve all prototypes as array of
	 * {@link Zend_Server_Reflection_Prototype Zend_Server_Reflection_Prototypes}
	 *
	 * @return array
	 */
	public function getPrototypes()
	{
		return $this->_prototypes;
	}

	/**
	 * Retrieve additional invocation arguments
	 *
	 * @return array
	 */
	public function getInvokeArguments()
	{
		return $this->_argv;
	}

	/**
	 * Wakeup from serialization
	 *
	 * Reflection needs explicit instantiation to work correctly. Re-instantiate
	 * reflection object on wakeup.
	 *
	 * @return void
	 */
	public function __wakeup()
	{
		if ($this->_reflection instanceof ReflectionMethod) {
			$class = new ReflectionClass($this->_class);
			$this->_reflection = new ReflectionMethod($class->newInstance(), $this->getName());
		} else {
			$this->_reflection = new ReflectionFunction($this->getName());
		}
	}
}
